#region Apache License
//
// Licensed to the Apache Software Foundation (ASF) under one or more 
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership. 
// The ASF licenses this file to you under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with 
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#endregion

using System;
using System.Globalization;
using System.Reflection;
using System.Text;

using log4net.Core;
using log4net.Util.TypeConverters;

namespace log4net.Util
{
  /// <summary>
  /// A convenience class to convert property values to specific types.
  /// </summary>
  /// <remarks>
  /// <para>
  /// Utility functions for converting types and parsing values.
  /// </para>
  /// </remarks>
  /// <author>Nicko Cadell</author>
  /// <author>Gert Driesen</author>
  public sealed class OptionConverter
  {
    #region Private Instance Constructors

    /// <summary>
    /// Initializes a new instance of the <see cref="OptionConverter" /> class. 
    /// </summary>
    /// <remarks>
    /// <para>
    /// Uses a private access modifier to prevent instantiation of this class.
    /// </para>
    /// </remarks>
    private OptionConverter()
    {
    }

    #endregion Private Instance Constructors

    #region Public Static Methods

    /// <summary>
    /// Converts a string to a <see cref="bool" /> value.
    /// </summary>
    /// <param name="argValue">String to convert.</param>
    /// <param name="defaultValue">The default value.</param>
    /// <returns>The <see cref="bool" /> value of <paramref name="argValue" />.</returns>
    /// <remarks>
    /// <para>
    /// If <paramref name="argValue"/> is "true", then <c>true</c> is returned. 
    /// If <paramref name="argValue"/> is "false", then <c>false</c> is returned. 
    /// Otherwise, <paramref name="defaultValue"/> is returned.
    /// </para>
    /// </remarks>
    public static bool ToBoolean(string argValue, bool defaultValue)
    {
      if (argValue != null && argValue.Length > 0)
      {
        try
        {
          return bool.Parse(argValue);
        }
        catch (Exception e)
        {
          LogLog.Error(declaringType, "[" + argValue + "] is not in proper bool form.", e);
        }
      }
      return defaultValue;
    }

    /// <summary>
    /// Parses a file size into a number.
    /// </summary>
    /// <param name="argValue">String to parse.</param>
    /// <param name="defaultValue">The default value.</param>
    /// <returns>The <see cref="long" /> value of <paramref name="argValue" />.</returns>
    /// <remarks>
    /// <para>
    /// Parses a file size of the form: number[KB|MB|GB] into a
    /// long value. It is scaled with the appropriate multiplier.
    /// </para>
    /// <para>
    /// <paramref name="defaultValue"/> is returned when <paramref name="argValue"/>
    /// cannot be converted to a <see cref="long" /> value.
    /// </para>
    /// </remarks>
    public static long ToFileSize(string argValue, long defaultValue)
    {
      if (argValue == null)
      {
        return defaultValue;
      }

      string s = argValue.Trim().ToUpperInvariant();
      long multiplier = 1;
      int index;

      if ((index = s.IndexOf("KB")) != -1)
      {
        multiplier = 1024;
        s = s.Substring(0, index);
      }
      else if ((index = s.IndexOf("MB")) != -1)
      {
        multiplier = 1024 * 1024;
        s = s.Substring(0, index);
      }
      else if ((index = s.IndexOf("GB")) != -1)
      {
        multiplier = 1024 * 1024 * 1024;
        s = s.Substring(0, index);
      }
      if (s != null)
      {
        // Try again to remove whitespace between the number and the size specifier
        s = s.Trim();

        long longVal;
        if (SystemInfo.TryParse(s, out longVal))
        {
          return longVal * multiplier;
        }
        else
        {
          LogLog.Error(declaringType, "OptionConverter: [" + s + "] is not in the correct file size syntax.");
        }
      }
      return defaultValue;
    }

    /// <summary>
    /// Converts a string to an object.
    /// </summary>
    /// <param name="target">The target type to convert to.</param>
    /// <param name="txt">The string to convert to an object.</param>
    /// <returns>
    /// The object converted from a string or <c>null</c> when the 
    /// conversion failed.
    /// </returns>
    /// <remarks>
    /// <para>
    /// Converts a string to an object. Uses the converter registry to try
    /// to convert the string value into the specified target type.
    /// </para>
    /// </remarks>
    public static object ConvertStringTo(Type target, string txt)
    {
      if (target == null)
      {
        throw new ArgumentNullException("target");
      }

      // If we want a string we already have the correct type
      if (typeof(string) == target || typeof(object) == target)
      {
        return txt;
      }

      // First lets try to find a type converter
      IConvertFrom typeConverter = ConverterRegistry.GetConvertFrom(target);
      if (typeConverter != null && typeConverter.CanConvertFrom(typeof(string)))
      {
        // Found appropriate converter
        return typeConverter.ConvertFrom(txt);
      }
      else
      {
#if NETSTANDARD1_3
        if (target.GetTypeInfo().IsEnum)
#else
        if (target.IsEnum)
#endif
        {
          // Target type is an enum.

          // Use the Enum.Parse(EnumType, string) method to get the enum value
          return ParseEnum(target, txt, true);
        }
        else
        {
          // We essentially make a guess that to convert from a string
          // to an arbitrary type T there will be a static method defined on type T called Parse
          // that will take an argument of type string. i.e. T.Parse(string)->T we call this
          // method to convert the string to the type required by the property.
          System.Reflection.MethodInfo meth = target.GetMethod("Parse", new Type[] { typeof(string) });
          if (meth != null)
          {
            // Call the Parse method
#if NETSTANDARD1_3
            return meth.Invoke(target, new[] { txt });
#else
            return meth.Invoke(null, BindingFlags.InvokeMethod, null, new object[] { txt }, CultureInfo.InvariantCulture);
#endif
          }
          else
          {
            // No Parse() method found.
          }
        }
      }
      return null;
    }

    //    /// <summary>
    //    /// Looks up the <see cref="IConvertFrom"/> for the target type.
    //    /// </summary>
    //    /// <param name="target">The type to lookup the converter for.</param>
    //    /// <returns>The converter for the specified type.</returns>
    //    public static IConvertFrom GetTypeConverter(Type target)
    //    {
    //      IConvertFrom converter = ConverterRegistry.GetConverter(target);
    //      if (converter == null)
    //      {
    //        throw new InvalidOperationException("No type converter defined for [" + target + "]");
    //      }
    //      return converter;
    //    }

    /// <summary>
    /// Checks if there is an appropriate type conversion from the source type to the target type.
    /// </summary>
    /// <param name="sourceType">The type to convert from.</param>
    /// <param name="targetType">The type to convert to.</param>
    /// <returns><c>true</c> if there is a conversion from the source type to the target type.</returns>
    /// <remarks>
    /// Checks if there is an appropriate type conversion from the source type to the target type.
    /// <para>
    /// </para>
    /// </remarks>
    public static bool CanConvertTypeTo(Type sourceType, Type targetType)
    {
      if (sourceType == null || targetType == null)
      {
        return false;
      }

      // Check if we can assign directly from the source type to the target type
      if (targetType.IsAssignableFrom(sourceType))
      {
        return true;
      }

      // Look for a To converter
      IConvertTo tcSource = ConverterRegistry.GetConvertTo(sourceType, targetType);
      if (tcSource != null)
      {
        if (tcSource.CanConvertTo(targetType))
        {
          return true;
        }
      }

      // Look for a From converter
      IConvertFrom tcTarget = ConverterRegistry.GetConvertFrom(targetType);
      if (tcTarget != null)
      {
        if (tcTarget.CanConvertFrom(sourceType))
        {
          return true;
        }
      }

      return false;
    }

    /// <summary>
    /// Converts an object to the target type.
    /// </summary>
    /// <param name="sourceInstance">The object to convert to the target type.</param>
    /// <param name="targetType">The type to convert to.</param>
    /// <returns>The converted object.</returns>
    /// <remarks>
    /// <para>
    /// Converts an object to the target type.
    /// </para>
    /// </remarks>
    public static object ConvertTypeTo(object sourceInstance, Type targetType)
    {
      Type sourceType = sourceInstance.GetType();

      // Check if we can assign directly from the source type to the target type
      if (targetType.IsAssignableFrom(sourceType))
      {
        return sourceInstance;
      }

      // Look for a TO converter
      IConvertTo tcSource = ConverterRegistry.GetConvertTo(sourceType, targetType);
      if (tcSource != null)
      {
        if (tcSource.CanConvertTo(targetType))
        {
          return tcSource.ConvertTo(sourceInstance, targetType);
        }
      }

      // Look for a FROM converter
      IConvertFrom tcTarget = ConverterRegistry.GetConvertFrom(targetType);
      if (tcTarget != null)
      {
        if (tcTarget.CanConvertFrom(sourceType))
        {
          return tcTarget.ConvertFrom(sourceInstance);
        }
      }

      throw new ArgumentException("Cannot convert source object [" + sourceInstance.ToString() + "] to target type [" + targetType.Name + "]", "sourceInstance");
    }

    //    /// <summary>
    //    /// Finds the value corresponding to <paramref name="key"/> in 
    //    /// <paramref name="props"/> and then perform variable substitution 
    //    /// on the found value.
    //    /// </summary>
    //    /// <param name="key">The key to lookup.</param>
    //    /// <param name="props">The association to use for lookups.</param>
    //    /// <returns>The substituted result.</returns>
    //    public static string FindAndSubst(string key, System.Collections.IDictionary props) 
    //    {
    //      if (props == null)
    //      {
    //        throw new ArgumentNullException("props");
    //      }
    //
    //      string v = props[key] as string;
    //      if (v == null) 
    //      {
    //        return null;    
    //      }
    //  
    //      try 
    //      {
    //        return SubstituteVariables(v, props);
    //      } 
    //      catch(Exception e) 
    //      {
    //        LogLog.Error(declaringType, "OptionConverter: Bad option value [" + v + "].", e);
    //        return v;
    //      }  
    //    }

    /// <summary>
    /// Instantiates an object given a class name.
    /// </summary>
    /// <param name="className">The fully qualified class name of the object to instantiate.</param>
    /// <param name="superClass">The class to which the new object should belong.</param>
    /// <param name="defaultValue">The object to return in case of non-fulfillment.</param>
    /// <returns>
    /// An instance of the <paramref name="className"/> or <paramref name="defaultValue"/>
    /// if the object could not be instantiated.
    /// </returns>
    /// <remarks>
    /// <para>
    /// Checks that the <paramref name="className"/> is a subclass of
    /// <paramref name="superClass"/>. If that test fails or the object could
    /// not be instantiated, then <paramref name="defaultValue"/> is returned.
    /// </para>
    /// </remarks>
    public static object InstantiateByClassName(string className, Type superClass, object defaultValue)
    {
      if (className != null)
      {
        try
        {
#if NETSTANDARD1_3
          Type classObj = SystemInfo.GetTypeFromString(superClass.GetTypeInfo().Assembly, className, true, true);
#else
          Type classObj = SystemInfo.GetTypeFromString(className, true, true);
#endif
          if (!superClass.IsAssignableFrom(classObj))
          {
            LogLog.Error(declaringType, "OptionConverter: A [" + className + "] object is not assignable to a [" + superClass.FullName + "] variable.");
            return defaultValue;
          }
          return Activator.CreateInstance(classObj);
        }
        catch (Exception e)
        {
          LogLog.Error(declaringType, "Could not instantiate class [" + className + "].", e);
        }
      }
      return defaultValue;
    }

    /// <summary>
    /// Performs variable substitution in string <paramref name="value"/> from the 
    /// values of keys found in <paramref name="props"/>.
    /// </summary>
    /// <param name="value">The string on which variable substitution is performed.</param>
    /// <param name="props">The dictionary to use to lookup variables.</param>
    /// <returns>The result of the substitutions.</returns>
    /// <remarks>
    /// <para>
    /// The variable substitution delimiters are <b>${</b> and <b>}</b>.
    /// </para>
    /// <para>
    /// For example, if props contains <c>key=value</c>, then the call
    /// </para>
    /// <para>
    /// <code lang="C#">
    /// string s = OptionConverter.SubstituteVariables("Value of key is ${key}.");
    /// </code>
    /// </para>
    /// <para>
    /// will set the variable <c>s</c> to "Value of key is value.".
    /// </para>
    /// <para>
    /// If no value could be found for the specified key, then substitution 
    /// defaults to an empty string.
    /// </para>
    /// <para>
    /// For example, if system properties contains no value for the key
    /// "nonExistentKey", then the call
    /// </para>
    /// <para>
    /// <code lang="C#">
    /// string s = OptionConverter.SubstituteVariables("Value of nonExistentKey is [${nonExistentKey}]");
    /// </code>
    /// </para>
    /// <para>
    /// will set <s>s</s> to "Value of nonExistentKey is []".   
    /// </para>
    /// <para>
    /// An Exception is thrown if <paramref name="value"/> contains a start 
    /// delimiter "${" which is not balanced by a stop delimiter "}". 
    /// </para>
    /// </remarks>
    public static string SubstituteVariables(string value, System.Collections.IDictionary props)
    {
      StringBuilder buf = new StringBuilder();

      int i = 0;
      int j, k;

      while (true)
      {
        j = value.IndexOf(DELIM_START, i);
        if (j == -1)
        {
          if (i == 0)
          {
            return value;
          }
          else
          {
            buf.Append(value.Substring(i, value.Length - i));
            return buf.ToString();
          }
        }
        else
        {
          buf.Append(value.Substring(i, j - i));
          k = value.IndexOf(DELIM_STOP, j);
          if (k == -1)
          {
            throw new LogException("[" + value + "] has no closing brace. Opening brace at position [" + j + "]");
          }
          else
          {
            j += DELIM_START_LEN;
            string key = value.Substring(j, k - j);

            string replacement = props[key] as string;

            if (replacement != null)
            {
              buf.Append(replacement);
            }
            i = k + DELIM_STOP_LEN;
          }
        }
      }
    }

    #endregion Public Static Methods

    #region Private Static Methods

    /// <summary>
    /// Converts the string representation of the name or numeric value of one or 
    /// more enumerated constants to an equivalent enumerated object.
    /// </summary>
    /// <param name="enumType">The type to convert to.</param>
    /// <param name="value">The enum string value.</param>
    /// <param name="ignoreCase">If <c>true</c>, ignore case; otherwise, regard case.</param>
    /// <returns>An object of type <paramref name="enumType" /> whose value is represented by <paramref name="value" />.</returns>
    private static object ParseEnum(System.Type enumType, string value, bool ignoreCase)
    {
#if !NETCF
      return Enum.Parse(enumType, value, ignoreCase);
#else
      FieldInfo[] fields = enumType.GetFields(BindingFlags.Public | BindingFlags.Static);

      string[] names = value.Split(new char[] {','});
      for (int i = 0; i < names.Length; ++i) 
      {
        names[i] = names [i].Trim();
      }

      long retVal = 0;

      try 
      {
        // Attempt to convert to numeric type
        return Enum.ToObject(enumType, Convert.ChangeType(value, typeof(long), CultureInfo.InvariantCulture));
      } 
      catch {}

      foreach (string name in names) 
      {
        bool found = false;
        foreach(FieldInfo field in fields) 
        {
          if (String.Compare(name, field.Name, ignoreCase) == 0) 
          {
            retVal |= ((IConvertible) field.GetValue(null)).ToInt64(CultureInfo.InvariantCulture);
            found = true;
            break;
          }
        }
        if (!found) 
        {
          throw new ArgumentException("Failed to lookup member [" + name + "] from Enum type [" + enumType.Name + "]");
        }
      }
      return Enum.ToObject(enumType, retVal);
#endif
    }

    #endregion Private Static Methods

    #region Private Static Fields

    /// <summary>
    /// The fully qualified type of the OptionConverter class.
    /// </summary>
    /// <remarks>
    /// Used by the internal logger to record the Type of the
    /// log message.
    /// </remarks>
    private static readonly Type declaringType = typeof(OptionConverter);

    private const string DELIM_START = "${";
    private const char DELIM_STOP = '}';
    private const int DELIM_START_LEN = 2;
    private const int DELIM_STOP_LEN = 1;

    #endregion Private Static Fields
  }
}
